// Copyright (C) 2012 Yury Bychenkov (http://bychenkov.com).
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//  - Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//  - Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//  - Neither the name of Yury Bychenkov nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "database.h"
#include "index.h"
#include "traits.h"
#include "dictionary.h"
#include "logchunk.h"
#include "nunchunk.h"
#include "csv.h"
#include "hashmap.h"
#include "filesystem.h"
#include "query.h"

#include "databasemanager.h"

#include "heap.h"

#include <iomanip>
using namespace std;

class PrintingConsumer {
	static const uint MAX_LINES_TO_PRINT = 50;
	static const uint MAX_LINE_SIZE = 256;
	static const uint COLUMN_WIDTH = 20;
	
	char lines[MAX_LINES_TO_PRINT][MAX_LINE_SIZE];
	uint count;
	vector<const Schema::Column*> columns;

public:

	PrintingConsumer(): count(0) {
	}

	void formatLine(Tuple& tuple, char* buffer, uint bufferSize) {
		stringstream ss;
		ss << "|";
		for (uint i = 0; i < columns.size(); i++) {
			const Schema::Column& column = *columns[i];
			uint columnWidth = max(COLUMN_WIDTH, (uint) column.name.size());
			ss << setw(columnWidth);
			char cellEnd = '|';
			
			switch (column.type) {
				case Schema::STRING: {
					uint8 v[4096];
					int size = tuple.nextBytes(v, sizeof(v));
					if (size == 0) {
						ss << left << "null";
					} else {
						if (size > (int) columnWidth) {
							cellEnd = '>';
							size = columnWidth;
						}
						v[size - 1] = 0;
						ss << left << v;
					}
					break;
				}
				
				case Schema::DECIMAL: {
					uint8 v[260];
					int size = tuple.nextBytes(v, sizeof(v));
					if (size == 0) {
						ss << right << "null";
					} else {
						NumberUtil::formatDecimal(ss << right, v);
					}
					break;
				}
				
				case Schema::INTEGER: {
					int64 v = tuple.nextInt64();
					if (v == IntegerTraits::NULL_VALUE) {
						ss << right << "null";
					} else {
						ss << right << v;
					}
					break;
				}
			}
			ss << setw(1) << cellEnd;
		}
		memcpy(buffer, ss.str().c_str(), min((uint) ss.str().size() + 1, bufferSize));
		buffer[bufferSize - 1] = 0;
	}
	
	void begin(const Schema& schema, const vector<uint> columnIndecies) {
		for (uint i = 0; i < columnIndecies.size(); i++) {
			columns.push_back(&schema.getColumn(columnIndecies[i]));
		}
	}
	
	bool put(const uint8* tupleData) {
		if (count < MAX_LINES_TO_PRINT) {
			Tuple tuple(tupleData);
			formatLine(tuple, lines[count], MAX_LINE_SIZE);
		}
	    count++;
		return true;
	}
	
	void end() {
		if (count == 0) {
			cout << "Nothing has found" << endl;
		} else {
			stringstream header;
			header << "|";
			for (uint i = 0; i < columns.size(); i++) {
				const Schema::Column& column = *columns[i];
				header << left << setw(max(COLUMN_WIDTH, (uint) column.name.size())) << column.name << "|";
			}
			header << setw(1) << endl << "-";
			for (uint i = 0; i < columns.size(); i++) {
				const Schema::Column& column = *columns[i];
				header << left << setw(max(COLUMN_WIDTH, (uint) column.name.size()) + 1) << setfill('-') << "-";
			}
			
			cout << header.str() << endl;
			
			for (uint i = 0; i < count && i < MAX_LINES_TO_PRINT; i++) {
				cout << lines[i] << endl;
			}
			if (count > MAX_LINES_TO_PRINT) {
			  cout << " ..." << endl;
			}
			cout << endl << count << " records found" << endl;
		}
	}
};

void usage() {
	cout << "Usage: nindex <dbdir> [--import <csvdir>]" << endl;
}

void querySyntax(const Schema& schema) {
	cout << endl;
	cout << "Query syntax:" << endl;
	cout << "    <select> [<where>] [<orderBy>]" << endl;
	cout << "where" << endl;
	cout << "    <select>  ::= select [top N] <field>, ..., <field>" << endl;
	cout << "    <where>   ::= where <filter>" << endl;
	cout << "    <filter>  ::= <field> [=|<|<=|>|>=|<>] <value> | <filter> [and|or] <filter> | not <filter> | (<filter>)" << endl;
	cout << "    <orderBy> ::= order by <field> [asc|desc], ... , <field> [asc|desc]" << endl;
	cout << "    <value>   ::= '...' | [+|-][0-9]+ | [+|-][0-9]+.[0-9]* | null" << endl;
	cout << "    <field>   ::=";
	for (uint i = 0; i < schema.getColumnCount(); i++) {
		const Schema::Column& column = schema.getColumn(i);
		cout << " " << column.name << ":" << Schema::formatType(column.type, false);
		if (i < schema.getColumnCount() - 1) {
		  cout << " |";
		}
		if (i > 0 && i % 4 == 0) {
			cout << endl << "                 ";
		}
	}
	cout << endl;
}

int importCsv(Database* db, const char* dir) {

	string csvDir(dir);
	vector<string> csvs = FileSystem::list(csvDir);
	csvDir += '/';

	RecordBuilder rb = db->newRecordBuilder();
	const Schema& schema = db->getSchema();

	uint64 nextSeqNum = 1;
		
	for (vector<string>::iterator it = csvs.begin(); it != csvs.end(); it++) {
		if (it->size() < 4 || 0 != strcmp(".csv", it->c_str() + it->size() - 4)) {
		  continue;
		}
		
		string csvFile = csvDir + *it;

		logger.info("Importing ", *it);

		CsvReader csv(csvFile.c_str());
	    
		for (uint rowIndex = 1; ; rowIndex++) {
			rb.reset();
	    
			while (csv.getRow() < (int) rowIndex && csv.next()) {
				// nothing to do		
			}
	    	
	    	rb.addInteger(nextSeqNum);

			for (uint columnIndex = 1; columnIndex < schema.getColumnCount() && csv.getRow() == (int) rowIndex; columnIndex++, csv.next()) {
				rb.add(csv.getValue());
				if (rb.isBad()) {
					break;
				}
			}
			if (rb.isBad() || !rb.isComplete()) {
				break;
			}

			if (!db->add(rb)) {
				break;
			}

			if (nextSeqNum % 100000 == 0) {
				logger.info(nextSeqNum, " rows have been imported so far");
			}

			nextSeqNum++;
		}

	}

	logger.info(nextSeqNum, " rows have been imported total");

	delete db;

	return 0;
}

int main(int argc, const char* args[]) {
	if (argc < 2 || 0 == strcmp("--help", args[1])) {
		usage();
		return 0;
	}
	const char* dbPath = args[1];
	const char* importPath = 0;

	if (argc >= 3) {
		if (0 != strcmp("--import", args[2]) || argc < 4) {
			usage();
			return 1;
		}
		importPath = args[3];
	}


	logger.info("NinjaIndex 0.1.0");
	
	DatabaseManager dbm;

	Database* db = dbm.open(dbPath);
	if (!db) {
		logger.error("Can't open database: ", dbPath);
		return 1;
	}

	if (importPath != 0) {
		importCsv(db, importPath);
		return 0;
	}

	dbm.startup();
	
	Thread::sleep(500);
	
	logger.info("Successfully started");
	
	querySyntax(db->getSchema());
	
	while (true) {
		char query[1024] = "";
		cout << endl << "ndx> ";
		cin.getline(query, sizeof(query));
		if (0 == strcmp("exit", query) || 0 == strcmp("quit", query)) {
			break;
		} else if (0 == strcmp("help", query)) {
			querySyntax(db->getSchema());
			continue;
		} else if ((uint) query[0] < 32) {
			continue;
		}
	
		cout << "Looking for: " << query << endl;

		PrintingConsumer consumer;
		uint count;
		clock_t t1 = clock();
		if (!db->search(query, consumer, count)) {
			cout << "Try 'help' command ..." << endl;
			continue;
		}
	    clock_t dt = ((clock() - t1) * 1000/ CLOCKS_PER_SEC);
	    
	    consumer.end();
		cout << count << " records scanned" << endl;
		cout << dt << " msecs elapsed" << endl;
	}

	return 0;
}
